﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Pahan.McuFusion.Common;
using Pahan.McuFusion.Common.ComponentModel;
using Pahan.McuFusion.Common.Utils;
using Pahan.McuFusion.Spi;

namespace Pahan.McuFusion.Atmel.AVR
{
    [Serializable]
    public class AvrSpiProgrammer : ProgrammerBase<AvrMcuInfo, AvrSpiProgrammerInfo, ISpiAdapter>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="T:System.Object"/> class.
        /// </summary>
        public AvrSpiProgrammer(AvrMcuInfo device, AvrSpiProgrammerInfo info, ISpiAdapter adapter)
            : base(device, info, adapter)
        {
            var eeprom = (AvrMemoryClassInfo)device.GetMemory("EEPROM");
            pagedEepromProgramming =
            supportsPagedEepromProgramming = 
                 eeprom != null && eeprom.SerialOptions.PageSize > 1;
        }

        [DefaultValue(ReliableReadingMode.ThreeSame)]
        [DisplayName("Reliable Reading")]
        [Description("If enabled, the application will read each byte several times, to ensure the result.")]
        public ReliableReadingMode ReliableReading
        {
            get { return reliableReading; }
            set { reliableReading = value; }
        }

        [DefaultValue(true)]
        [DisplayName("Use Paged EEPROM Programming")]
        [Description("Use paged EEPROM programming if supported by the MCU.")]
        public bool PagedEepromProgramming
        {
            get { return pagedEepromProgramming && supportsPagedEepromProgramming; }
            set
            { 
                pagedEepromProgramming = value && supportsPagedEepromProgramming;
            }
        }

        #region Overrides of ProgrammerBase<AvrMcuInfo,AvrSpiProgrammerInfo,SpiAdapter>

        /// <summary>
        /// Performs an actual action to open the programmer.
        /// </summary>
        protected override void DoOpen()
        {
            var instruction = GetProgrammingEnableInstruction();

            var result = Adapter.PerformIO(instruction);
            if (result[2] != instruction[1])
            {
                WaitHelper.Wait(1000000);
                result = Adapter.PerformIO(instruction);
                if (result[2] != instruction[1])
                {
                    throw new InvalidOperationException("Communication is out of synchronization.");
                }
            }

            VerifySignature();
        }

        /// <summary>
        /// Read the device memory.
        /// </summary>
        /// <param name="arg">
        /// Contains the instance of <see cref="MemoryClassInfo"/> and
        /// the output stream for the memory content.
        /// </param>
        /// <param name="context">Asynchronous operation context.</param>
        protected override void DoReadMemory(MemoryArgument arg, AsyncContext context)
        {
            Func<uint, IList<byte>> getReadInstruction = null;
            switch (arg.Memory.Name)
            {
                case "Program Memory":
                    getReadInstruction = GetReadProgramMemoryInstruction;
                    break;
                case "EEPROM":
                    getReadInstruction = GetReadEepromInstruction;
                    break;
                default:
                    throw new InvalidOperationException();
            }
            using (PriorityScope.Realtime)
            {
                for (uint i = 0; i < arg.Memory.MemorySize; i++)
                {
                    if (context.CancellationPending)
                    {
                        context.Cancel = true;
                        break;
                    }
                    byte data = ReadMemoryByte(i, getReadInstruction);
                    arg.Stream.WriteByte(data);
                    context.ReportProgress((int)i * 100 / arg.Memory.MemorySize);
                }
            }
        }

        /// <summary>
        /// Writes the device memory.
        /// </summary>
        /// <param name="arg">
        /// Contains the instance of <see cref="MemoryClassInfo"/> and
        /// the intput stream with the memory content.
        /// </param>
        /// <param name="context">Asynchronous operation context.</param>
        protected override void DoWriteMemory(MemoryArgument arg, AsyncContext context)
        {
            var memory = (AvrMemoryClassInfo)arg.Memory;
            if (memory.SerialOptions.PageSize > 1 &&
                (memory.Name != "EEPROM" || pagedEepromProgramming))
            {
                WritePagedMemoryAsync(arg, context);
            }
            else
            {
                WriteNonPagedMemoryAsync(arg, context);
            }
        }

        /// <summary>
        /// Erases the device memory.
        /// </summary>
        /// <param name="arg">
        /// Contains the instance of <see cref="MemoryClassInfo"/>.
        /// </param>
        /// <param name="context">Asynchronous operation context.</param>
        protected override void DoEraseMemory(MemoryArgument arg, AsyncContext context)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Private members

        private void VerifySignature()
        {
            var result = Adapter.PerformIO(GetReadSignatureInstruction());
            var signature = new[] { result[3], result[7], result[11] };
        }

        private byte ReadMemoryByte(uint address, Func<uint, IList<byte>> getReadInstruction)
        {
            switch (ReliableReading)
            {
                case ReliableReadingMode.Disabled:
                    return Adapter.PerformIO(getReadInstruction(address))[3];
                case ReliableReadingMode.ThreeSame:
                    {
                        var values = new Dictionary<byte, byte>();
                        while (true)
                        {
                            var result = Adapter.PerformIO(getReadInstruction(address))[3];
                            if (values.ContainsKey(result))
                            {
                                if (values[result] == 2)
                                    return result;
                                else
                                    values[result]++;
                            }
                            else
                                values.Add(result, 1);
                        }
                    }
                case ReliableReadingMode.FiveAttempts:
                    {
                        var result = Adapter.PerformIO(getReadInstruction(address));
                        var data = from b in new[] { result[3], result[7], result[11], result[15], result[19] }
                                   group b by b
                                       into g
                                       where g.Count() >= 3
                                       select g.Key;
                        if (data.Any())
                            return data.First();
                        else
                            throw new Exception("An error occured while reading at address: " + address.ToString("X8"));
                    }
                default:
                    throw new Exception("Invalid mode.");
            }
        }

        private void WritePagedMemoryAsync(MemoryArgument arg, AsyncContext context)
        {
            Func<uint, byte, IList<byte>> getLoadInstruction = null;
            Func<uint, IList<byte>> getWriteInstruction = null;
            var memory = (AvrMemoryClassInfo)arg.Memory;
            switch (memory.Name)
            {
                case "Program Memory":
                    getLoadInstruction = GetLoadProgramMemoryInstruction;
                    getWriteInstruction = GetWriteProgramMemoryInstruction;
                    break;
                case "EEPROM":
                    getLoadInstruction = GetLoadEepromInstruction;
                    getWriteInstruction = GetWriteEepromInstruction;
                    break;
                default:
                    throw new InvalidOperationException();
            }

            uint pageSize = (uint)memory.SerialOptions.PageSize;
            var buf = new byte[memory.MemorySize];
            int len = arg.Stream.Read(buf, 0, buf.Length);
            if (len < buf.Length)
            {
                for (int i = len; i < buf.Length; i++)
                    buf[i] = arg.Memory.EmptyValue;
            }
            using (PriorityScope.Realtime)
            {
                for (uint baseAddress = 0; baseAddress < buf.Length; baseAddress += pageSize)
                {
                    if (context.CancellationPending)
                    {
                        context.Cancel = true;
                        break;
                    }

                    for (uint offset = 0; offset < pageSize; offset++)
                    {
                        uint address = baseAddress + offset;
                        Adapter.PerformIO(getLoadInstruction(address, buf[address]));
                    }
                    Adapter.PerformIO(getWriteInstruction(baseAddress));
                    context.ReportProgress((int)baseAddress * 100 / memory.MemorySize);
                    WaitHelper.Wait(memory.SerialOptions.WriteDelay);
                }
            }
        }

        private void WriteNonPagedMemoryAsync(MemoryArgument arg, AsyncContext context)
        {
            Func<uint, byte, IList<byte>> getWriteInstruction = null;
            var memory = (AvrMemoryClassInfo)arg.Memory;
            switch (memory.Name)
            {
                case "EEPROM":
                    getWriteInstruction = GetWriteEepromByteInstruction;
                    break;
                default:
                    throw new InvalidOperationException();
            }
            byte[] data = new byte[arg.Stream.Length];
            arg.Stream.Read(data, 0, data.Length);
            using (PriorityScope.Realtime)
            {
                for (uint i = 0; i < data.Length; i++)
                {
                    if (context.CancellationPending)
                    {
                        context.Cancel = true;
                        break;
                    }
                    Adapter.PerformIO(getWriteInstruction(i, data[i]));
                    context.ReportProgress((int)i * 100 / memory.MemorySize);
                    WaitHelper.Wait(memory.SerialOptions.WriteDelay);
                }
            }
        }

        private ReliableReadingMode reliableReading = ReliableReadingMode.ThreeSame;

        private bool pagedEepromProgramming;

        private readonly bool supportsPagedEepromProgramming;

        #endregion

        #region SPI Commands

        private IList<byte> GetProgrammingEnableInstruction()
        {
            return new byte[] { 0xAC, 0x53, 0x00, 0x00 };
        }

        private IList<byte> GetReadSignatureInstruction()
        {
            return new byte[]
                       {
                           0x30, 0x00, 0x00, 0x00,
                           0x30, 0x00, 0x01, 0x00,
                           0x30, 0x00, 0x02, 0x00
                       };
        }

        private IList<byte> GetReadProgramMemoryInstruction(uint address)
        {
            byte byte0 = (byte)(0x20 | (address & 0x01) << 3);
            byte byte1 = (byte)(address >> 9);
            byte byte2 = (byte)(address >> 1);
            const byte byte3 = 0;

            return ReliableReading != ReliableReadingMode.FiveAttempts ?
                new byte[] { byte0, byte1, byte2, byte3 } :
                new byte[]
                    {
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                    };

        }

        private IList<byte> GetLoadProgramMemoryInstruction(uint address, byte value)
        {
            byte byte0 = (byte)(0x40 | (address & 0x01) << 3);
            byte byte1 = (byte)(address >> 9);
            byte byte2 = (byte)(address >> 1);
            byte byte3 = value;
            return new byte[] { byte0, byte1, byte2, byte3 };
        }

        private IList<byte> GetWriteProgramMemoryInstruction(uint address)
        {
            const byte byte0 = 0x4C;
            byte byte1 = (byte)(address >> 9);
            byte byte2 = (byte)(address >> 1);
            const byte byte3 = 0;
            return new byte[] { byte0, byte1, byte2, byte3 };
        }

        private IList<byte> GetReadEepromInstruction(uint address)
        {
            const byte byte0 = 0xA0;
            byte byte1 = (byte)(address >> 8);
            byte byte2 = (byte)(address);
            const byte byte3 = 0;

            return ReliableReading != ReliableReadingMode.FiveAttempts ?
                new byte[] { byte0, byte1, byte2, byte3 } :
                new byte[]
                    {
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                        byte0, byte1, byte2, byte3,
                    };

        }

        private IList<byte> GetLoadEepromInstruction(uint address, byte value)
        {
            const byte byte0 = 0xC1;
            byte byte1 = 0x00;
            byte byte2 = (byte)address;
            byte byte3 = value;
            return new byte[] { byte0, byte1, byte2, byte3 };
        }

        private IList<byte> GetWriteEepromInstruction(uint address)
        {
            const byte byte0 = 0xC2;
            byte byte1 = (byte)(address >> 8);
            byte byte2 = (byte)address;
            const byte byte3 = 0;
            return new byte[] { byte0, byte1, byte2, byte3 };
        }

        private IList<byte> GetWriteEepromByteInstruction(uint address, byte value)
        {
            const byte byte0 = 0xC0;
            byte byte1 = (byte)(address >> 8);
            byte byte2 = (byte)(address);
            byte byte3 = value;

            return new[] { byte0, byte1, byte2, byte3 };
        }

        #endregion

        #region Nested Classes

        [TypeConverter(typeof(EnumTypeConverter))]
        public enum ReliableReadingMode
        {
            [Description("Disabled")]
            Disabled,

            [Description("Read until 3 same values")]
            ThreeSame,

            [Description("5 attempts")]
            FiveAttempts,
        }

        #endregion
    }
}
